/************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * Copyright 2009, 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0. You can also
 * obtain a copy of the License at http://odftoolkit.org/docs/license.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ************************************************************************/
package schema2template.model;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;


/**
 * Collection Class for RelaxNG definitions of an Element, Attribute, Value or Datatype.
 *
 * <p>Conventions: <ul><li>PuzzlePiece sorting is done by ns:local tag names as first key and hashCode as second key (see class PuzzlePiece).</li>
 * <li>Since it is a Collection, PuzzlePieceSet is not meant to be used in a Collection. So equals(o) and hashCode() are not overwritten</li>
 * <li>All returned PuzzlePieceSet objects are immutable to protect them against
 * naive usage in velocity templates</li></ul></p>
 */
public class PuzzlePieceSet implements QNamedPuzzleComponent, Collection<PuzzlePiece> {

    private boolean mImmutable = false;
    private SortedSet<PuzzlePiece> mDefinitions;

    public PuzzlePieceSet() {
        mDefinitions = new TreeSet<PuzzlePiece>();
    }

    public PuzzlePieceSet(Collection<PuzzlePiece> c) {
        mDefinitions = new TreeSet<PuzzlePiece>(c);
    }

    private void assertNotImmutable() {
        if (mImmutable) {
            throw new RuntimeException("Attempt to change an immutable DefinitionSet.");
        }
    }

    private void assertNotEmpty(String plannedAction) {
        if (this.size()==0) {
            throw new RuntimeException("Attempt to " + plannedAction + " of empty DefinitionSet ");
        }
    }
    
    private void assertMultiples(String plannedAction) {
        assertNotEmpty(plannedAction);
        PuzzlePiece first = first();
        MSVExpressionType type = first.getType();
        String name = first.getQName();
        for (PuzzlePiece def : this) {
            if (!type.equals(def.getType())) {
                throw new RuntimeException("Attempt to " + plannedAction + " of DefinitionSet consisting of different types of Definition objetcs.");
            }
            String defname = def.getQName();
            if ((name == null && defname != null) || !name.equals(defname)) {
                throw new RuntimeException("Attempt to " + plannedAction + " of DefinitionSet consisting of differently named Definition objetcs.");
            }
        }
    }

    public boolean equals(Object o) {
        return (o instanceof PuzzlePieceSet && ((PuzzlePieceSet) o).mDefinitions.equals(mDefinitions)) ? true : false;
    }

    public int hashCode() {
        return mDefinitions.hashCode();
    }
    
    private PuzzlePiece first() {
        return this.iterator().next();
    }

    /* Unite Definitions with equal content
     *
     * Returns a Map of the lost Definitions to their survived counterparts
     */
    Map<PuzzlePiece, PuzzlePiece> uniteDefinitionsWithEqualContent() {
        Map<PuzzlePiece, PuzzlePiece> retval = new HashMap<PuzzlePiece, PuzzlePiece>();
        SortedSet<PuzzlePiece> immutableSet = new TreeSet<PuzzlePiece>(this.mDefinitions);
        for (PuzzlePiece def1 : immutableSet) {
            if (!this.mDefinitions.contains(def1)) {
                // if def1 is already removed, we shouldn't process it
                continue;
            }
            for (PuzzlePiece def2 : immutableSet.tailSet(def1)) {
                if (def1 == def2) {
                    // Don't compare def1 to def1
                    continue;
                }
                if (!this.mDefinitions.contains(def2)) {
                    //if def2 is already removed, we shouldn't process it
                    continue;
                }
                if (!def1.getQName().equals(def2.getQName())) {
                    break;
                }
                // Now test for content equality
                if (def1.contentEquals(def2)) {
                    // remove from map
                    this.remove(def2);
                    // map lost to survived counterpart
                    retval.put(def2, def1);
                    // mix parent information
                    def1.getParents().addAll(def2.getParents());
                }
            }
        }
        // Remove deleted multiples in a separate step since in the first step the needed
        // information wasn't present (note that Multiples are no synonym for Definitions with equal content!)
        for (PuzzlePiece def : this.mDefinitions) {
            PuzzlePieceSet immutableDups = new PuzzlePieceSet(def.withMultiples());
            for (PuzzlePiece multiple : immutableDups) {
                if (multiple != def) {
                    if (!this.mDefinitions.contains(multiple)) {
                        def.withMultiples().remove(multiple);
                    }
                }
            }
        }
        return retval;
    }
    
    /**
     * Make PuzzlePieceSet immutable. Cannot be undone.
     *
     * Template Usage: Not for use in templates as all PuzzlePieceSet already have been made immutable.
     */
    public void makeImmutable() {
        mImmutable = true;
    }

    /**
     * <p>Returns new PuzzlePieceSet containing the elements of this PuzzlePieceSet, but restricted to one
     * PuzzlePiece per Name.</p>
     *
     * <p>Template Usage: #foreach ($element in $elements.withoutMultiples())</p>
     *
     * @return new PuzzlePieceSet
     */
    public PuzzlePieceSet withoutMultiples() {
        Map<String, PuzzlePiece> uniqueMap = new HashMap<String, PuzzlePiece>();
        for (PuzzlePiece def : this) {
            uniqueMap.put(def.getQName(), def);
        }
        return new PuzzlePieceSet(uniqueMap.values());
    }

    /**
     * <p>Returns new PuzzlePieceSet containing the elements of this PuzzlePieceSet, but without
     * the elements of the parameter removeAll</p>
     *
     * <p>Template Usage: #set ($non_base_attributes = $element.getAttributes().without($baseclass.getAttributes())</p>
     *
     * @param removeAll QNamedPuzzleComponent which (or which elements) should be removed from the new PuzzlePieceSet
     * @return new PuzzlePieceSet
     */
    public PuzzlePieceSet without(QNamedPuzzleComponent removeAll) {
        PuzzlePieceSet retval = new PuzzlePieceSet(this);
        retval.removeAll(removeAll.getCollection());
        return retval;
    }

    /**
     * Returns new PuzzlePieceSet containing the elements of this PuzzlePieceSet, but only those
     * which have at least one element from the QNamedPuzzleComponent parameter as one of their parent Definitions.
     *
     * <p>Template Usage: Imagine we have one attribute name and we're not interested in the differences
     * between Definitions sharing the same name. We're now printing the
     * resulting allowed attribute values for each parent element name: </p>
     * <code><br />
     * #set ( $oneOrMoreAttributes = $model.getAttribute($atttributename) )<br />
     * ## we want to write information about only _one_ parent per Name...<br />
     * #foreach ($parent in $oneOrMoreAttributes.getParents().withoutMultiples())<br/>
     * - Allowed Values for Parent Element $parent :<br />
     * ## but we want the attribute values displayed which are allowed in _all_ parents with the same Name...<br />
     * #foreach ($value in $oneOrMoreAttributes.byParent($parent.withMultiples()).getValues())<br />
     *    -- "$value" <br />
     * #end<br />
     * #end
     * </code><br />
     *
     * @param parents
     * @return new PuzzlePieceSet
     */
    public PuzzlePieceSet byParent(QNamedPuzzleComponent parents) {
        PuzzlePieceSet retval = new PuzzlePieceSet();
        for (PuzzlePiece def : this) {
            PuzzlePieceSet defparents = def.getParents();
            for (PuzzlePiece parent : parents.getCollection()) {
                if (defparents.contains(parent)) {
                    retval.add(def);
                    break;
                }
            }
        }
        return retval;
    }

    /**
     * Check whether this List contains an Element by this Name
     *
     * @param aDefinitionName
     * @return True if an element by this name exists
     */
    public boolean containsName(String aDefinitionName) {
        for (PuzzlePiece def : this) {
            if (def.getQName().equals(aDefinitionName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check whether this List contains an Element by this Name
     *
     * @param aNamed
     * @return True if an element by this name exists
     */
    public boolean containsName(QNamed aNamed) {
        return containsName(aNamed.getQName());
    }

/*
 * -----------------------------------------------------
 *  Interface QNamed
 * -----------------------------------------------------
 */


    /**
     * Gets the ns:local tag name of the Definitions - provided that this PuzzlePieceSet
     * is not empty and all Definitions share the same tag name. Throws Exception otherwise.
     *
     * @return The tag name
     */
    public String getQName() {
        assertMultiples("get name");
        return first().getQName();
    }

    /**
     * Gets the type of the Definitions - provided that this PuzzlePieceSet
     * is not empty and all Definitions have the same type and name. Throws Exception otherwise.
     */
    public MSVExpressionType getType() {
        assertMultiples("get type");
        return first().getType();
    }

    /**
     * Determines whether the Definitions can have text - provided that this PuzzlePieceSet
     * is not empty and all Definitions have the same type and name. Throws Exception otherwise.
     */
    public boolean canHaveText() {
        assertMultiples("determine text availability");
        for (PuzzlePiece def : this) {
            if (def.canHaveText()) {
                return true;
            }
        }
        return false;
    }

    public boolean isSingleton(PuzzleComponent child) {
        for (PuzzlePiece def : this) {
            if (!def.isSingleton(child)) {
                return false;
            }
        }
        return true;
    }

    public String getLocalName() {
        return XMLModel.extractLocalname(getQName());
    }

    public String getNamespace() {
        return XMLModel.extractNamespace(getQName());
    }

    /**
     * <p>Returns String representation (convenient method for getQName())</p>
     *
     * <p>Template Usage: Just use $aDefinitionSet as you would use a string variable</p>
     */
    public String toString() {
        return getQName();
    }

/*
 * -----------------------------------------------------
 *  Interface Collection<PuzzlePiece>
 * -----------------------------------------------------
 */

    public boolean add(PuzzlePiece e) {
        assertNotImmutable();
        return mDefinitions.add(e);
    }

    public boolean addAll(Collection<? extends PuzzlePiece> c) {
        assertNotImmutable();
        return mDefinitions.addAll(c);
    }

    public void clear() {
        assertNotImmutable();
        mDefinitions.clear();
    }

    public boolean contains(Object o) {
        return (o instanceof PuzzlePiece) ? mDefinitions.contains((PuzzlePiece) o) : false;
    }

    public boolean containsAll(Collection<?> c) {
        return mDefinitions.containsAll(c);
    }

    public boolean isEmpty() {
        return mDefinitions.isEmpty();
    }

    public Iterator<PuzzlePiece> iterator() {
        return mDefinitions.iterator();
    }

    public boolean remove(Object o) {
        assertNotImmutable();
        return (o instanceof PuzzlePiece) ? mDefinitions.remove((PuzzlePiece) o) : false;
    }

    public boolean removeAll(Collection<?> c) {
        assertNotImmutable();
        return mDefinitions.removeAll(c);
    }

    public boolean retainAll(Collection<?> c) {
        assertNotImmutable();
        return mDefinitions.retainAll(c);
    }

    public int size() {
        return mDefinitions.size();
    }

    public Object[] toArray() {
        return mDefinitions.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return mDefinitions.toArray(a);
    }

/*
 * -----------------------------------------------------
 *  Interface QNamedPuzzleComponent
 * -----------------------------------------------------
 */

    public PuzzlePieceSet getChildElements() {
        PuzzlePieceSet retval = new PuzzlePieceSet();
        for (PuzzlePiece def : this) {
            retval.addAll(def.getChildElements());
        }
        return retval;
    }

    public boolean isMandatory(QNamedPuzzleComponent child) {
        for (PuzzlePiece def : this) {
            if (def.isMandatory(child)) {
                return true;
            }
        }
        return false;
    }

    public Collection<PuzzlePiece> getCollection() {
        return mDefinitions;
    }

    public PuzzlePieceSet getAttributes() {
        PuzzlePieceSet retval = new PuzzlePieceSet();
        for (PuzzlePiece def : this) {
            retval.addAll(def.getAttributes());
        }
        return retval;
    }

    public PuzzlePieceSet getDatatypes() {
        PuzzlePieceSet retval = new PuzzlePieceSet();
        for (PuzzlePiece def : this) {
            retval.addAll(def.getDatatypes());
        }
        return retval;
    }

    public PuzzlePieceSet getParents() {
        PuzzlePieceSet retval = new PuzzlePieceSet();
        for (PuzzlePiece def : this) {
            retval.addAll(def.getParents());
        }
        return retval;
    }


    public PuzzlePieceSet getValues() {
        PuzzlePieceSet retval = new PuzzlePieceSet();
        for (PuzzlePiece def : this) {
            retval.addAll(def.getValues());
        }
        return retval;
    }

}
